"use strict"

class this.Shape

  @defaults = { x: 0, y: 0, w: 100, h: 100 }

  constructor: (elm, options = {}, data) ->
    @_i = data._i if data
    @data = data
    settings = $.extend true, {}, Shape.defaults, options
    @parent = undefined
    @children = []
    @settings = settings
    @svg = createSvg(@, elm, settings)
    @redraw settings

  toHash: () ->
    children = (c.toHash() for c in @children) if @children.length
    _.extend {}, @data, { _t: @settings.type, elements: children }
    # extent: do (ex = @getExtent(), p_ex = @parent?.getExtent()) ->
    #   if p_ex
    #     x: ex.x - p_ex.x, y: ex.y - p_ex.y, w: ex.w, h: ex.h
    #   else
    #     x: ex.x, y: ex.y, w: ex.w, h: ex.h

  getExtent2: (child) ->
    p_ex = _.extend {}, @getExtent()
    for c in @children when c != child
      p_ex.y = Math.max(p_ex.y, c.getExtent().y1)
    p_ex

  getExtent: () ->
    @extent ||= do (d = @svg.container.data()[0]) ->
      [x, y, w, h] = [d.x, d.y, d.w, d.h]
      Object.freeze x: x, y: y, w: w, h: h, x1: x + w, y1: y + h, cx: x + w / 2, cy: y + h / 2

  resetExtent: () ->
    @extent = null
    @

  select: (select = true) ->
    @svg.el.classed('selected', select)

  setParent: (parent, options) ->
    @parent && @resetParent()
    @parent = parent.addChild(@)
    @

  resetParent: (options) ->
    if @parent
      @parent.removeChild @
      @parent = null
    @

  remove: () ->
    @resetParent()
    @children[0].remove() while @children.length
    @svg.el.remove()
    core.removeShape @
    delete @

  addChild: (child) ->
    unless child in @children
      @children.push child
      @alignChild child
      #FIXME
      window.global.page.selectAll('.shape').sort (a, b) ->
        if b.shape && _.contains(b.shape.children, a.shape) then 1
        else 0
    @

  removeChild: (child) ->
    if (idx = @children.indexOf(child)) >= 0
      @children.splice idx, 1
    @

  setDropTarget: (child, options) ->
    options = getDropableOptions @, child
    @svg.el.classed('dropable_target', true)
    @drawDropableArea(child, options)
    @

  resetDropTarget: () ->
    @svg.el.classed('dropable_target', false)
    @svg.el.select('.drop_rect').remove()
    @

  drawDropableArea: (child, options) ->
    el = @svg.el
    ex = getDroppedExtent @, child, options
    rect = el.select('rect.drop_rect')
    rect.empty() && rect = el.append('rect').classed('drop_rect', true)
    rect.attr('x', ex.x).attr('y', ex.y).attr('width', ex.w).attr('height', ex.h)
      .attr('stroke', 'none')
      .attr("fill-opacity", .1)
    @

  alignChild: (child) ->
    options = getDropableOptions @, child
    ex = getDroppedExtent @, child, options
    child.redraw x: ex.x, y: ex.y, w: ex.w, h: ex.h
    @

  redraw: (options = {}) ->
    prev_ex = @getExtent()

    g = @svg.container
    d = g.data()[0]
    for a in ['x', 'y', 'w', 'h']
      if options[a]? then d[a] = options[a]

    extent = @resetExtent().getExtent()

    g.attr('transform', "translate(#{extent.x},#{extent.y})")
    g.selectAll(".resize").attr("transform", (d) -> "translate(#{if /e$/.test(d.resize) then extent.w else 0}, #{if /^s/.test(d.resize) then extent.h else 0})")
    g.selectAll(".rect, .n > rect, .s > rect").attr("width", extent.w)
    g.selectAll(".rect, .e > rect, .w > rect").attr("height", extent.h)
    g.select('.label').attr('x', extent.w / 2).attr('y', extent.h / 2).attr("height", extent.h).attr("width", extent.w)

    if extent.w != prev_ex.w || extent.h != prev_ex.h
      # FIXME
      @alignChild(c) for c in @children
    else if (dx = extent.x - prev_ex.x) | (dy = extent.y - prev_ex.y)
      for c in @children
        do (ex = c.getExtent()) ->
          c.redraw x: ex.x + dx, y: ex.y + dy
    @

  updateLabel: (text) ->
    @svg.container.select('.label').text(text)

getDroppedExtent = (parent, child, options) ->
  c_ex = child.resetExtent().getExtent()
  if c = _.find(parent.children, (c) -> c != child && Util.isOverlapping(c.getExtent(), c_ex))
    ex = c.getExtent()
    p_ex = _.extend {}, parent.getExtent(), { y: ex.y }
  else
    p_ex = parent.getExtent2 child

  w = Math.min c_ex.w, p_ex.w
  h = Math.min c_ex.h, p_ex.h

  x_0 = () -> p_ex.x
  x_1 = () -> p_ex.x1 - w
  x_2 = () -> (if c_ex.cx < p_ex.cx then x_0 else x_1)()
  x_f = () -> Util.trimValue(c_ex.x, p_ex.x, p_ex.x1 - c_ex.w)
  y_0 = () -> p_ex.y
  y_1 = () -> p_ex.y1 - h
  y_2 = () -> (if c_ex.cy < p_ex.cy then y_0 else y_1)()
  y_f = () -> Util.trimValue(c_ex.y, p_ex.y, p_ex.y1 - c_ex.h)

  if options.width == 'max'
    x  = p_ex.x
    x1 = p_ex.x1
    w  = p_ex.w
  else
    x = (do ->
      if      ( options.align.left && !options.align.right) then x_0
      else if (!options.align.left &&  options.align.right) then x_1
      else if ( options.align.left &&  options.align.right) then x_2
      else x_f)()

  if options.height == 'max'
    y  = p_ex.y
    y1 = p_ex.y1
    h  = p_ex.h
  else
    y = (do ->
      if      ( options.align.top && !options.align.bottom) then y_0
      else if (!options.align.top &&  options.align.bottom) then y_1
      else if ( options.align.top &&  options.align.bottom) then y_2
      else y_f)()

  range =
    x: { min: p_ex.x, max: p_ex.x1 - w }
    y: { min: p_ex.y, max: p_ex.y1 - h }
  rg = roundGrid(range)
  x = rg.x(x)
  y = rg.y(y)

  { x: x, y: y, x1: x + w, y1: y + h, w: w, h: h }

createSvg = (shape, elm, settings) ->
  behavior = settings.behavior || {}
  callbacks = settings.callbacks || {}

  svg = elm.append('g')
    .classed('shape', true)
    .classed("shape-#{settings.type}", true)
  container = svg.data([{}])
    .append("g")
    .style('pointer-events', 'all')
  bg_rect = container.append("rect").classed('rect', true)
    .attr("fill-opacity", .1)
  unless settings.type == 'page' || settings.type == 'trash'
    container.append('text').classed('label', true)
      .attr('text-anchor', 'middle')
      .text(shape.data.ref || shape.data.value || settings.type)

  if behavior.resize.x || behavior.resize.y
    svg.on "mouseover", () -> enableResize(shape, settings)

  dragstart = (d) ->
    d.shape = shape # for reordering
    d.dx = d.dy = 0
    d.extent = shape.getExtent()
    d.bounded = boundedMove(d.extent, {
      min_x: settings.bounding.x,
      min_y: settings.bounding.y,
      max_x: settings.bounding.x1,
      max_y: settings.bounding.y1
    })
    core.selectShape shape._i
    callbacks.onMoveStart && callbacks.onMoveStart(shape)

  drag = (d) ->
    if behavior.move.x
      d.dx += d3.event.dx ; x = d.bounded.x(d.extent.x + d.dx)
    if behavior.move.y
      d.dy += d3.event.dy ; y = d.bounded.y(d.extent.y + d.dy)
    shape.redraw x: x, y: y
    callbacks.onMove && callbacks.onMove(shape, { x: x, y: y, x1: x + d.extent.w, y1: y + d.extent.h })

  dragend = (d) ->
    callbacks.onMoveEnd && callbacks.onMoveEnd(shape)

  if settings.behavior.move.x || settings.behavior.move.y
    container
      .attr("cursor", "move")
      .call(d3.behavior.drag().origin(Object).on("dragstart", dragstart).on("drag", drag).on("dragend", dragend))

  return {
    el: svg,
    container: container,
    bg_rect: bg_rect
  }

enableResize = (shape, settings) ->
  container = shape.svg.container
  return if container.classed('resizable')
  container.classed('resizable', true)

  behavior = settings.behavior
  onResize = settings.callbacks.onResize

  csidx = 0 + (if behavior.resize.x then 0 else 2) + (if behavior.resize.y then 0 else 1)
  data = ({ resize: rs } for rs in d3_svg_brushResizes[csidx])
  tz = container.selectAll(".resize").data(data)

  min = 10
  max = 300

  dragstart = (d) ->
    /[e]$/.test(d.resize) && d.x_field = 'x1'
    /[w]$/.test(d.resize) && d.x_field = 'x'
    /^[s]/.test(d.resize) && d.y_field = 'y1'
    /^[n]/.test(d.resize) && d.y_field = 'y'
    d.dx = 0
    d.dy = 0
    d.x = d3.event.sourceEvent.pageX
    d.y = d3.event.sourceEvent.pageY
    d.extent = shape.getExtent();
    d.bounded = boundedResize(d.extent, {
      min_x: settings.bounding.x,
      min_y: settings.bounding.y,
      max_x: settings.bounding.x1,
      max_y: settings.bounding.y1,
      min_w: 50,
      min_h: 20
    })
    container.style("pointer-events", "none")
    d3.select("body").style("cursor", d3.select(this).style("cursor"))

  drag = (d) ->
    d.x_field && d.dx = (d3.event.sourceEvent.pageX - d.x)
    d.y_field && d.dy = (d3.event.sourceEvent.pageY - d.y)
    if d3.event.dx || d3.event.dy
      tx = if d.x_field then d.bounded[d.x_field](d.dx) else d.extent.x + d.dx
      ty = if d.y_field then d.bounded[d.y_field](d.dy) else d.extent.y + d.dy
      # d3.select(this).attr('transform', "translate(#{tx - d.extent.x}, #{ty - d.extent.y})")
      nx  = if d.x_field == 'x'  then tx else d.extent.x
      ny  = if d.y_field == 'y'  then ty else d.extent.y
      nx1 = if d.x_field == 'x1' then tx else d.extent.x1
      ny1 = if d.y_field == 'y1' then ty else d.extent.y1
      shape.redraw x: nx, y: ny, w: nx1 - nx, h: ny1 - ny
      onResize && onResize({ x: nx, y: ny, x1: nx1, y1: ny1 })

  dragend = (d) ->
    container.style("pointer-events", "all")
    d3.select("body").style("cursor", null)

  tz.enter().append("g")
    .attr("class", (d) -> "resize " + d.resize)
    .attr("cursor", (d) -> d3_svg_brushCursor[d.resize])
    .call(d3.behavior.drag().origin(Object).on("dragstart", dragstart).on("drag", drag).on("dragend", dragend))
    .append("rect")
      .attr("x", (d) -> if /[ew]$/.test(d.resize) then -3 else null)
      .attr("y", (d) -> if /^[ns]/.test(d.resize) then -3 else null)
      .attr("width", 6)
      .attr("height", 6)
      .attr('fill-opacity', 0.1)
      .style("visibility", "hidden")
  tz.exit().remove();
  shape.redraw()

disableResize = (container) ->
  container.selectAll(".resize").data([]).exit().remove()

boundedMove = (extent, options) ->
  range =
    x: { min: options.min_x, max: options.max_x - extent.w }
    y: { min: options.min_y, max: options.max_y - extent.h }
  rg = roundGrid() #roundGrid(range)
  return {
    x: (x) -> Util.trimValue(rg.x(x), range.x.min, range.x.max)
    y: (y) -> Util.trimValue(rg.y(y), range.y.min, range.y.max)
  }

boundedResize = (extent, options) ->
  # return { x: _.identity, y: _.identity, x1: _.identity, y1: _.identity }
  range =
    x: { min: options.min_x, max: extent.x1 - options.min_w }
    y: { min: options.min_y, max: extent.y1 - options.min_h }
    x1: { min: extent.x + options.min_w, max: options.max_x }
    y1: { min: extent.y + options.min_h, max: options.max_y }
  rg = roundGrid(range)
  return {
    x: (d) -> Util.trimValue(rg.x(extent.x + d), range.x.min, range.x.max)
    y: (d) -> Util.trimValue(rg.y(extent.y + d), range.y.min, range.y.max)
    x1: (d) -> Util.trimValue(rg.x1(extent.x1 + d), range.x1.min, range.x1.max)
    y1: (d) -> Util.trimValue(rg.y1(extent.y1 + d), range.y1.min, range.y1.max)
  }

roundGrid = (range) ->
  # return { x: _.identity, y: _.identity, x1: _.identity, y1: _.identity }
  step = { x: 60, y: 30 }
  if range
    return {
      x: (x) -> range.x.min + ~~((x + step.x / 2 - range.x.min) / step.x) * step.x
      y: (y) -> range.y.min + ~~((y + step.y / 2 - range.y.min) / step.y) * step.y
      x1: (x) -> range.x.min + ~~((x + step.x / 2 - range.x.min) / step.x) * step.x
      y1: (y) -> range.y.min + ~~((y + step.y / 2 - range.y.min) / step.y) * step.y
    }
  else
    return { x: _.identity, y: _.identity }

d3_svg_brushCursor = {
  n: "ns-resize",
  e: "ew-resize",
  s: "ns-resize",
  w: "ew-resize",
  nw: "nwse-resize",
  ne: "nesw-resize",
  se: "nwse-resize",
  sw: "nesw-resize"
}

d3_svg_brushResizes = [
  ["n", "e", "s", "w", "nw", "ne", "se", "sw"],
  ["e", "w"],
  ["n", "s"],
  []
]
